package com.google.code.p.narcissus.core;

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;

import javax.imageio.ImageIO;

import org.apache.log4j.Logger;

import com.google.code.p.narcissus.core.BrowserConfiguration.BrowserCoordinates;
import com.google.code.p.narcissus.core.model.ComparisonResult;
import com.google.code.p.narcissus.core.model.ComparisonResult.PictureComparisonResult;
import com.google.code.p.narcissus.core.model.ReferencePicture;
import com.google.code.p.narcissus.core.reference.ReferenceMetaDataMapper;
import com.google.code.p.narcissus.core.screenshots.PictureComparisonMapper;
import com.thoughtworks.selenium.Selenium;

/**
 * <p>
 * Initial screen capture capabilities written by Ivo.
 * </p>
 * <p>
 * As the development and the corrections of the ScreenShooter can get quite complex, know that, to help, the LOGGER Logger for this ScreenShooter will log lots of messages and the
 * LOGGER_PICS Logger will save temporary screenshots when necessary. Put these Logger level to "debug" if you wish to see all the information possible.
 * </p>
 * 
 * @author Ivo Krab
 * @author jerome JADOULLE [JADOJ] - 30 juil. 2010
 */
public class ScreenShooter implements IScreenShooter {

	public static final String NARCISSUS_POLICY_SAVE_IF_NO_DIFFERENCE = "narcissus.policy.saveIfNoDifference";

	public static final String NARCISSUS_POLICY_SAVE_IF_EXCLUDED_DIFFERENCE = "narcissus.policy.saveIfExcludedDifference";

	private static final String SCREENSHOT_EXTENSION = "png";

	private static final String GENERATED_ID_PREFIX = "NarcissusIdGen-";

	private static final String CSS_CLIENT_WIDTH = "clientWidth";

	private static final String CSS_CLIENT_HEIGHT = "clientHeight";

	private static final String CSS_SCROLL_LEFT = "scrollLeft";

	private static final String CSS_SCROLL_TOP = "scrollTop";

	private static final String SCREEENSHOT_EXTENSION_PNG = ".png";

	private static final Logger LOGGER = Logger.getLogger(ScreenShooter.class);

	private static final Logger LOGGER_PICS = Logger.getLogger("narcissus.resources.debugPictures");

	public static final String REFERENCES_FOLDER_NAME = "references";

	private static int seleniumSleepInterval = 0;

	private File screenshotsRoot;

	private String testPath;

	private String testName;

	private Selenium session;

	private static String TEST_RUN_FOLDER;
	static {
		SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH'h'mm");
		TEST_RUN_FOLDER = format.format(new Date()) + " - testRun";
	}

	private static String TEST_DEBUG_FOLDER;
	static {
		SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH'h'mm");
		TEST_DEBUG_FOLDER = format.format(new Date()) + " - Debug";
	}

	public ScreenShooter(String screenshotsRootPath, String testPath, String testName, Selenium session) {
		screenshotsRoot = new File(screenshotsRootPath);
		this.testPath = testPath;
		this.testName = testName;
		this.session = session;
	}

	public ScreenShooter(String screenshotsRootPath, Selenium session) {
		this(screenshotsRootPath, null, null, session);
	}

	public String getTestPath() {
		return testPath;
	}

	public void setTestPath(String testPath) {
		this.testPath = testPath;
	}

	public String getTestName() {
		return testName;
	}

	public void setTestName(String testName) {
		this.testName = testName;
	}

	public static int getSeleniumSleepInterval() {
		return seleniumSleepInterval;
	}

	public static void setSeleniumSleepInterval(int intervalInMilliseconds) {
		ScreenShooter.seleniumSleepInterval = intervalInMilliseconds;
	}

	/**
	 * {@inheritDoc}
	 */
	public PictureComparisonResult verifyGuiElement(String elementLocator, String screenshotName) throws NarcissusException {
		PictureComparisonResult result;

		BufferedImage screenshot = getPicture(elementLocator, screenshotName);

		ReferencePicture reference = getReferencePicture(screenshotName);
		if (reference != null) {
			ComparisonResult comparisonResult = compareScreenshotToReference(screenshot, reference);

			result = comparisonResult.getResult();

			if (saveScreenShotIfNoDifferencefound() || needsToSaveScreenshot(result, reference.getReferenceMetaData() != null)) {
				saveScreenshotToTestFolder(screenshot, screenshotName);
			}
			saveComparisonResultDetailsToDisk(screenshotName, comparisonResult);
		} else {
			saveScreenshotAsReference(screenshot, screenshotName);
			result = PictureComparisonResult.noReferenceFound;
		}

		return result;
	}

	private boolean needsToSaveScreenshot(PictureComparisonResult result, boolean exclusionZonesDefined) {
		return saveScreenShotIfNoDifferencefound() || PictureComparisonResult.verificationFail.equals(result) || (exclusionZonesDefined && saveScreenShotIfDifferenceZonesDefined());
	}

	private boolean saveScreenShotIfNoDifferencefound() {
		return "true".equals(System.getProperty(ScreenShooter.NARCISSUS_POLICY_SAVE_IF_NO_DIFFERENCE, "false"));
	}

	private boolean saveScreenShotIfDifferenceZonesDefined() {
		return "true".equals(System.getProperty(ScreenShooter.NARCISSUS_POLICY_SAVE_IF_EXCLUDED_DIFFERENCE, "false"));
	}

	private void saveComparisonResultDetailsToDisk(String screenshotName, ComparisonResult comparisonResultDetails) throws NarcissusException {
		File comparisonResultFile = new File(getTestFolder(), "/" + screenshotName + ".xml");
		PictureComparisonMapper.toXml(comparisonResultDetails, comparisonResultFile);
	}

	private ReferencePicture getReferencePicture(String screenshotName) throws NarcissusException {
		ReferencePicture referencePicture = null;
		BufferedImage reference = getReferenceByName(screenshotName);
		if (reference != null) {
			referencePicture = new ReferencePicture(reference, ReferenceMetaDataMapper.fromXml(getExclusionMetaDataFile(screenshotName)));
		}

		return referencePicture;
	}

	private BufferedImage getReferenceByName(String screenshotName) throws NarcissusException {
		BufferedImage referenceScreenshot = null;
		File referenceScreenshotsFolder = new File(screenshotsRoot, REFERENCES_FOLDER_NAME + "/" + testPath + "/" + testName);
		if (referenceScreenshotsFolder.exists() && referenceScreenshotsFolder.isDirectory() && referenceScreenshotsFolder.canRead()) {
			File referenceScreenshotFile = new File(referenceScreenshotsFolder, screenshotName + SCREEENSHOT_EXTENSION_PNG);
			if (referenceScreenshotFile.exists() && referenceScreenshotFile.isFile() && referenceScreenshotFile.canRead()) {
				try {
					referenceScreenshot = ImageIO.read(referenceScreenshotFile);
				} catch (IOException impossibleToReadReferenceEx) {
					LOGGER.fatal(
					    "Impossible to read reference screenshot '" + referenceScreenshotFile.getName() + "' from reference directory[" + referenceScreenshotsFolder.getAbsolutePath() + "]",
					    impossibleToReadReferenceEx);
					throw new NarcissusException(this.getClass().getName() + ".impossibleToReadReferenceEx", impossibleToReadReferenceEx);
				}
			}
		}

		return referenceScreenshot;
	}

	private File getExclusionMetaDataFile(String screenshotName) {
		File exclusionMetaDataFile = new File(screenshotsRoot, REFERENCES_FOLDER_NAME + "/" + testPath + "/" + testName + "/" + screenshotName + ".xml");
		return exclusionMetaDataFile.exists() && exclusionMetaDataFile.isFile() ? exclusionMetaDataFile : null;
	}

	private boolean saveScreenshotToTestFolder(BufferedImage screenshot, String screenshotName) throws NarcissusException {
		return saveScreenshotToFolder(screenshot, screenshotName, getTestFolder());
	}

	private boolean saveScreenshotToFolder(BufferedImage screenshot, String screenshotName, File savefolder) throws NarcissusException {
		File saveFolder = savefolder;
		saveFolder.mkdirs();
		if (!saveFolder.isDirectory() || !saveFolder.canWrite()) {
			throw new NarcissusException(this.getClass().getName() + ".unusableTestRunFolderEx");
		}

		File screenshotFileName = new File(saveFolder, screenshotName + SCREEENSHOT_EXTENSION_PNG);

		boolean screenshotAlreadyExisted = screenshotFileName.exists();

		try {
			ImageIO.write(screenshot, SCREENSHOT_EXTENSION, screenshotFileName);
		} catch (IOException impossibleToWriteScreenshotEx) {
			LOGGER.error("Impossible to write screenshot '" + screenshotName + ".png' to disk[Location: " + saveFolder.getAbsolutePath() + "]", impossibleToWriteScreenshotEx);
			throw new NarcissusException(this.getClass().getName() + ".impossibleToWriteScreenshotEx", impossibleToWriteScreenshotEx);
		}

		return screenshotAlreadyExisted;
	}

	private File getTestFolder() {
		return new File(screenshotsRoot, TEST_RUN_FOLDER + "/" + testPath + "/" + testName);
	}

	private File getDebugFolder() {
		return new File(screenshotsRoot, TEST_DEBUG_FOLDER + "/" + testPath + "/" + testName);
	}

	private ComparisonResult compareScreenshotToReference(BufferedImage screenshot, ReferencePicture reference) throws NarcissusException {
		return new PictureComparator().compare(reference, screenshot);
	}

	/**
	 * @return true if there was already a reference screenshot at the same name, false if not
	 * @throws NarcissusException
	 *           if it was impossible to save the picture into the folder.
	 */
	private boolean saveScreenshotAsReference(BufferedImage screenshot, String screenshotName) throws NarcissusException {
		File saveFolder = new File(screenshotsRoot, REFERENCES_FOLDER_NAME + "/" + testPath + "/" + testName);
		saveFolder.mkdirs();
		if (!saveFolder.isDirectory() || !saveFolder.canWrite()) {
			throw new NarcissusException(this.getClass().getName() + ".unusableReferenceFolderEx");
		}

		File screenshotFileName = new File(saveFolder, screenshotName + SCREEENSHOT_EXTENSION_PNG);

		boolean referenceAlreadyExisted = screenshotFileName.exists();

		try {
			ImageIO.write(screenshot, SCREENSHOT_EXTENSION, screenshotFileName);
		} catch (IOException impossibleToWriteScreenshotEx) {
			LOGGER.error("Impossible to write screenshot '" + screenshotName + ".png' to disk[Location: " + saveFolder.getAbsolutePath() + "]", impossibleToWriteScreenshotEx);
			throw new NarcissusException(this.getClass().getName() + ".impossibleToWriteScreenshotEx", impossibleToWriteScreenshotEx);
		}

		return referenceAlreadyExisted;
	}

	private BufferedImage getPicture(String locator, String screenshotName) throws NarcissusException {
		ScreenCaptureRobot robot = new ScreenCaptureRobot();
		if (isBrowserWindowTooBigForScreen(robot)) {
			throw new NarcissusException(this.getClass().getName() + ".browserWindowBiggerThanScreenEx");
		}

		BufferedImage takenPicture = null;
		session.setSpeed(Integer.valueOf(ScreenShooter.seleniumSleepInterval).toString());
		session.windowFocus(); // Makes sure browser window has the focus

		session.getEval("window.moveTo(0,0)"); // make sure window is in position where we can find it

		try {
			if (session.isElementPresent(locator)) {
				String parentElementLocator = scrollWindowAndAncestorsToElement(locator);

				if (isScrollableElement(locator)) {
					LOGGER.error("Current version of Narcissus cannot take screenshots of scrollable elements - Work in progress...");
					throw new NarcissusException(this.getClass().getName() + ".elementIsScrollableEx");
				}
				if (isElementFullyOnScreen(locator)) {
					takenPicture = captureScreenshot(locator, robot);
				} else {
					if (parentElementLocator == null) {
						takenPicture = captureTooBigElementScreenshots(locator, robot);
					} else {
						takenPicture = captureElementInScrollableParentScreenshots(locator, screenshotName, parentElementLocator, robot);
					}
				}

				return takenPicture;
			} else {
				throw new NarcissusException(this.getClass().getName() + ".elementNotFoundEx");
			}
		} catch (Exception anyEx) {
			LOGGER.error("Impossible to take screenshot for locator " + locator, anyEx);
			throw new NarcissusException(this.getClass().getName() + ".impossibleToTakeScreenshotEx");
		} finally {
			resetWindowAndAncestorsToOrigin(locator);
		}
	}

	private boolean isScrollableElement(String locator) {
		StringBuilder builder = new StringBuilder();
		builder.append(setInt(locator, CSS_SCROLL_TOP, 0));
		builder.append(setInt(locator, CSS_SCROLL_LEFT, 0));

		builder.append(setInt(locator, CSS_SCROLL_TOP, getInt(locator, CSS_CLIENT_HEIGHT)));
		builder.append(setInt(locator, CSS_SCROLL_LEFT, getInt(locator, CSS_CLIENT_WIDTH)));

		builder.append("var element = this.browserbot.findElement(\"" + locator + "\");");
		builder.append("var isElementScrollable = element." + CSS_SCROLL_TOP + " > 0 || element." + CSS_SCROLL_LEFT + " > 0;");

		// Reset to origin
		builder.append(setInt(locator, CSS_SCROLL_TOP, 0));
		builder.append(setInt(locator, CSS_SCROLL_LEFT, 0));
		builder.append("isElementScrollable;");

		String result = session.getEval(builder.toString());
		return Boolean.parseBoolean(result);
	}

	private boolean isElementBiggerThanScreen(String locator) {
		Dimension documentSize = BrowserConfiguration.getDocumentSize(session);
		return (isElementWiderThanScreen(locator, documentSize) || isElementTallerThanScreen(locator, documentSize));
	}

	private boolean isElementWiderThanScreen(String locator, Dimension documentSize) {
		int elementPositionLeft = getAbsolutePositioningToAncestor(locator, null).x;
		int elementWidth = session.getElementWidth(locator).intValue();
		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("Element: " + locator);
			LOGGER.debug("positionLeft: " + elementPositionLeft + " | width: " + elementWidth + " => totalSize: " + (elementPositionLeft + elementWidth));
			LOGGER.debug("Document width: " + documentSize.width + "\n");
		}
		return elementPositionLeft + elementWidth > documentSize.width;
	}

	private boolean isElementTallerThanScreen(String locator, Dimension documentSize) {
		int elementPositionTop = getAbsolutePositioningToAncestor(locator, null).y;
		int elementHeight = session.getElementHeight(locator).intValue();
		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("Element: " + locator);
			LOGGER.debug("positionTop: " + elementPositionTop + " | height: " + elementHeight + " => totalSize: " + (elementPositionTop + elementHeight));
			LOGGER.debug("Document Height: " + documentSize.height + "\n");
		}
		return (elementPositionTop + elementHeight) > documentSize.height;
	}

	private boolean isElementFullyOnScreen(String locator) {
		return !isElementBiggerThanScreen(locator);
	}

	private int getInt(String locator, String property) {
		return Integer.parseInt(session.getEval("this.browserbot.findElement(\"" + locator + "\")." + property + ";"));
	}

	private String getString(String locator, String property) {
		return session.getEval("this.browserbot.findElement(\"" + locator + "\")." + property + ";");
	}

	private String setInt(String locator, String property, int value) {
		return "this.browserbot.findElement(\"" + locator + "\")." + property + " = " + value + ";";
	}

	/**
	 * <p>
	 * Known limitations:<br />
	 * We oversee that, in specific cases, where multiple scrollable parents enter in action, it is possible that this method will fail to produce the expected result. As it is quite
	 * hard to think about it not having a specific failing example, we will leave it in the air for now and correct if the need arises.
	 * </p>
	 */
	private BufferedImage captureTooBigElementScreenshots(String locator, ScreenCaptureRobot robot) throws NarcissusException {

		BrowserCoordinates browserCoordinates = BrowserConfiguration.getBrowserCoordinates(session);
		BufferedImage totalImage = null;

		int elementPositionLeft = session.getElementPositionLeft(locator).intValue();
		int elementPositionTop = session.getElementPositionTop(locator).intValue();
		Rectangle elementDimension = new Rectangle(elementPositionLeft, elementPositionTop, session.getElementWidth(locator).intValue(), session.getElementHeight(locator).intValue());

		Dimension documentSize = BrowserConfiguration.getDocumentSize(session);
		Rectangle currentScreenshotDimension = new Rectangle(elementPositionLeft, elementPositionTop, 0, 0);

		int windowXOffset = elementDimension.x;
		while (currentScreenshotDimension.x < elementDimension.x + elementDimension.width) {
			if (currentScreenshotDimension.x + documentSize.width < elementDimension.x + elementDimension.width) {
				currentScreenshotDimension.width = documentSize.width;
			} else {
				currentScreenshotDimension.width = elementDimension.x + elementDimension.width - currentScreenshotDimension.x;
			}
			int windowYOffset = elementDimension.y;
			while (currentScreenshotDimension.y < elementDimension.y + elementDimension.height) {
				if (currentScreenshotDimension.y + documentSize.height < elementDimension.y + elementDimension.height) {
					currentScreenshotDimension.height = documentSize.height;
				} else {
					currentScreenshotDimension.height = elementDimension.y + elementDimension.height - currentScreenshotDimension.y;
				}

				// TAKING Screenshot
				scrollWindowToLocation(windowXOffset, windowYOffset);
				// Beware: It is NOT because we ask to scroll to a specific location that this exact location is scrolled to (if end of document is reached)!
				windowXOffset = getWindowXScroll();
				windowYOffset = getWindowYScroll();

				// I now have the precise size of the screenshot to take.
				if (LOGGER.isDebugEnabled()) {
					LOGGER.debug("Base screenshot dimension: " + currentScreenshotDimension);
					LOGGER.debug("window X scroll: " + windowXOffset);
					LOGGER.debug("window Y scroll: " + windowYOffset);
					LOGGER.debug("ACTUAL SCREENSHOT TAKEN: " + (browserCoordinates.winX + currentScreenshotDimension.x - windowXOffset) + ", "
					    + (currentScreenshotDimension.y - windowYOffset) + " - " + currentScreenshotDimension.width + ", " + browserCoordinates.winY + currentScreenshotDimension.height
					    + "\n");
				}

				BufferedImage capturedPart = captureScreenshot(new Rectangle(browserCoordinates.winX + currentScreenshotDimension.x - windowXOffset, browserCoordinates.winY
				    + currentScreenshotDimension.y - windowYOffset, currentScreenshotDimension.width, currentScreenshotDimension.height), robot);

				if (totalImage == null) {
					totalImage = new BufferedImage(elementDimension.width, elementDimension.height, BufferedImage.TYPE_INT_ARGB);
				}

				writeToImage(capturedPart, totalImage, currentScreenshotDimension.x, currentScreenshotDimension.y);

				windowYOffset += currentScreenshotDimension.height;
				currentScreenshotDimension.y += currentScreenshotDimension.height;
			}
			windowXOffset += currentScreenshotDimension.width;
			currentScreenshotDimension.x += currentScreenshotDimension.width;
		}
		return totalImage;
	}

	private BufferedImage captureElementInScrollableParentScreenshots(String locator, String screenshotName, String scrollableAncestorLocator, ScreenCaptureRobot robot)
	    throws NarcissusException {
		BufferedImage totalImage = null;
		BrowserCoordinates browserCoordinates = BrowserConfiguration.getBrowserCoordinates(session);
		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("Document coordinates: " + browserCoordinates);
		}

		// Position is here calculated relative to the scrollable parent element
		Rectangle scrollableAncestorDimension = new Rectangle(session.getElementPositionLeft(scrollableAncestorLocator).intValue(), session.getElementPositionTop(
		    scrollableAncestorLocator).intValue(), session.getElementWidth(scrollableAncestorLocator).intValue(), session.getElementHeight(scrollableAncestorLocator).intValue());

		Point offsetToAncestor = getAbsolutePositioningToAncestor(locator, scrollableAncestorLocator);
		Rectangle elementDimension = new Rectangle(offsetToAncestor.x, offsetToAncestor.y, session.getElementWidth(locator).intValue(), session.getElementHeight(locator).intValue());

		Dimension documentSize = BrowserConfiguration.getDocumentSize(session);

		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("Scrollable ancestor dimension: " + scrollableAncestorDimension);
			LOGGER.debug("Element dimension - relative to ancestor: " + elementDimension);
			LOGGER.debug("Document size: " + documentSize);
		}

		if (scrollableAncestorDimension.x + scrollableAncestorDimension.width > documentSize.width
		    || scrollableAncestorDimension.y + scrollableAncestorDimension.height > documentSize.height) {
			throw new NarcissusException("com.google.code.p.narcissus.core.ScreenShooter.scrollableParentTooBigEx");
		}

		// At this point, we are sure that the parent fully fits in the currently displayed part of the screen. Taking screenshot will be easier.

		Rectangle currentScreenshotDimension = new Rectangle(elementDimension.x, elementDimension.y, 0, 0);

		int currentScreenshot = 1;

		int scrollableAncestorXOffset = elementDimension.x;
		int totalElementWidth = elementDimension.x + elementDimension.width;

		int screenshotMaxWidth = scrollableAncestorDimension.width - elementDimension.x; // takes the initial offset from element to its parent into account

		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("-- CurrentScreenshotDimension.x: " + currentScreenshotDimension.x + "; totalElementWidth: " + totalElementWidth);
		}
		while (currentScreenshotDimension.x < totalElementWidth) {
			if (currentScreenshotDimension.x + screenshotMaxWidth < totalElementWidth) {
				currentScreenshotDimension.width = screenshotMaxWidth;
			} else {
				currentScreenshotDimension.width = totalElementWidth - currentScreenshotDimension.x;
			}

			int scrollableAncestorYOffset = elementDimension.y;
			int totalElementHeight = elementDimension.y + elementDimension.height;
			int screenshotMaxHeight = scrollableAncestorDimension.height - elementDimension.y; // takes the initial offset from element to its parent into
			// account

			if (LOGGER.isDebugEnabled()) {
				LOGGER.debug("-- CurrentScreenshotDimension.y: " + currentScreenshotDimension.y + "; totalElementHeight: " + totalElementHeight);
			}
			while (currentScreenshotDimension.y < totalElementHeight) {
				if (currentScreenshotDimension.y + screenshotMaxHeight < totalElementHeight) {
					currentScreenshotDimension.height = screenshotMaxHeight;
				} else {
					currentScreenshotDimension.height = totalElementHeight - currentScreenshotDimension.y;
				}

				// TAKING Screenshot
				scrollElementToLocation(scrollableAncestorLocator, scrollableAncestorXOffset, scrollableAncestorYOffset);
				// Beware: It is NOT because we ask to scroll to a specific location that this exact location is scrolled to (if end of scrollable ancestor
				// content is reached)!
				int actualAncestorXOffset = getInt(scrollableAncestorLocator, "scrollLeft");
				int actualAncestorYOffset = getInt(scrollableAncestorLocator, "scrollTop");

				// I now have the precise size of the screenshot to take.
				int windowOffsetX = scrollableAncestorDimension.x + scrollableAncestorXOffset - actualAncestorXOffset;
				int windowOffsetY = scrollableAncestorDimension.y + scrollableAncestorYOffset - actualAncestorYOffset;

				if (LOGGER.isDebugEnabled()) {
					LOGGER.debug("Base screenshot dimension: " + currentScreenshotDimension);
					LOGGER.debug("Scrollable ancestor X scroll: " + actualAncestorXOffset);
					LOGGER.debug("Scrollable ancestor Y scroll: " + actualAncestorYOffset);
					LOGGER.debug("ACTUAL SCREENSHOT TAKEN: " + windowOffsetX + ", " + windowOffsetY + " - " + currentScreenshotDimension.width + ", " + currentScreenshotDimension.height
					    + "\n");
				}

				BufferedImage capturedPart = captureScreenshot(new Rectangle(browserCoordinates.winX + windowOffsetX, browserCoordinates.winY + windowOffsetY,
				    currentScreenshotDimension.width, currentScreenshotDimension.height), robot);

				if (LOGGER_PICS.isDebugEnabled()) {
					saveScreenshotToFolder(capturedPart, screenshotName + "-debug-part" + currentScreenshot, getDebugFolder());
				}
				currentScreenshot++;

				if (totalImage == null) {
					totalImage = new BufferedImage(elementDimension.width, elementDimension.height, capturedPart.getType());
				}
				if (LOGGER.isDebugEnabled()) {
					LOGGER.debug("Writing image part to X: " + (currentScreenshotDimension.x - offsetToAncestor.x) + ", Y: " + (currentScreenshotDimension.y - offsetToAncestor.y));
				}
				writeToImage(capturedPart, totalImage, currentScreenshotDimension.x - offsetToAncestor.x, currentScreenshotDimension.y - offsetToAncestor.y);

				scrollableAncestorYOffset += currentScreenshotDimension.height;
				currentScreenshotDimension.y += currentScreenshotDimension.height;

				if (LOGGER.isDebugEnabled()) {
					LOGGER.debug("-- CurrentScreenshotDimension.y: " + currentScreenshotDimension.y + "; totalElementHeight: " + totalElementHeight);
				}
			}
			scrollableAncestorXOffset += currentScreenshotDimension.width;
			currentScreenshotDimension.x += currentScreenshotDimension.width;

			// resetting the height
			scrollableAncestorYOffset = elementDimension.y;
			currentScreenshotDimension.y = elementDimension.y;

			if (LOGGER.isDebugEnabled()) {
				LOGGER.debug("-- CurrentScreenshotDimension.x: " + currentScreenshotDimension.x + "; totalElementWidth: " + totalElementWidth);
			}
		}
		return totalImage;
	}

	private void writeToImage(BufferedImage capturedPart, BufferedImage totalImage, int x, int y) throws NarcissusException {
		Graphics graphics = totalImage.getGraphics();
		graphics.drawImage(capturedPart, x, y, null);
		graphics.dispose();
	}

	private int getWindowXScroll() {
		StringBuilder builder = new StringBuilder();
		if (BrowserConfiguration.isIE(session)) {
			builder.append("document.body.scrollLeft;");
		} else {
			builder.append("window.pageXOffset;");
		}

		return Integer.parseInt(session.getEval(builder.toString()));
	}

	private int getWindowYScroll() {
		StringBuilder builder = new StringBuilder();
		if (BrowserConfiguration.isIE(session)) {
			builder.append("document.body.scrollTop;");
		} else {
			builder.append("window.pageYOffset;");
		}

		return Integer.parseInt(session.getEval(builder.toString()));
	}

	private BufferedImage captureScreenshot(String locator, ScreenCaptureRobot robot) throws NarcissusException {
		BrowserCoordinates browserCoordinates = BrowserConfiguration.getBrowserCoordinates(session);
		int elementX, elementY, elementWidth, elementHeight;

		Point elementAbsolutePosition = getAbsolutePositioningToAncestor(locator, null);

		elementX = elementAbsolutePosition.x + browserCoordinates.winX;
		elementY = elementAbsolutePosition.y + browserCoordinates.winY;
		elementWidth = session.getElementWidth(locator).intValue();
		elementHeight = session.getElementHeight(locator).intValue();

		// We now have the correct location of the element to take a screenshot of. let's take the shot then!
		Rectangle snapRect = new Rectangle(elementX, elementY, elementWidth, elementHeight);

		return captureScreenshot(snapRect, robot);
	}

	private BufferedImage captureScreenshot(Rectangle snapRect, ScreenCaptureRobot robot) throws NarcissusException {
		robot.mouseMove(0, 0);
		return robot.createScreenCapture(snapRect);
	}

	private boolean isBrowserWindowTooBigForScreen(ScreenCaptureRobot robot) throws NarcissusException {
		Dimension screenSize = robot.getScreenSize();
		Dimension windowSize = BrowserConfiguration.getWindowSize(session);
		return windowSize.getWidth() > screenSize.getWidth() || windowSize.getHeight() > screenSize.getHeight();
	}

	/**
	 * Makes sure the element will be on screen when capture happens.
	 * <p>
	 * Technically: Method cycles through each parent until the window element.<br />
	 * Each scrollable ancestor is scrolled to have the element to take a screenshot of as near as possible to the top-left view point (it is not because we ask the window to the
	 * element that the element will be in the top left position. Indeed, if the end of the page is reached, the window scroll won't go further.)<br />
	 * Then, when each scrollable parent is updated, we finally ask the
	 * 
	 * </p>
	 * <p>
	 * Note: To take in account when taking screenshots then...
	 * </p>
	 * 
	 * @return an id locator to the first scrollable element's parent if any exist, null if not.
	 */
	private String scrollWindowAndAncestorsToElement(String locator) {

		String currentElementLocator = locator;

		String parentElementLocator = getParentElementId(currentElementLocator);
		String firstScrollableParentLocator = parentElementLocator;

		int[] scrolls, totalScrolls = new int[] { 0, 0 };
		while (parentElementLocator != null) {
			if (isScrollableElement(parentElementLocator)) {
				scrolls = scrollElementToView(currentElementLocator, parentElementLocator);
				totalScrolls[0] += scrolls[0];
				totalScrolls[1] += scrolls[1];
			}

			parentElementLocator = getParentElementId(parentElementLocator);
		}

		// All the ancestors of the element have been updated. Put the window on the element as well.
		StringBuilder builder = new StringBuilder();
		builder.append("var currentElement = this.browserbot.findElement(\"" + locator + "\");");
		builder.append("var parentElement = currentElement.parentNode;");
		builder.append("var totalOffsetLeft = 0;");
		builder.append("var totalOffsetTop = 0;");
		builder.append("while(parentElement.localName != 'html'){");
		builder.append("totalOffsetLeft = totalOffsetLeft + (currentElement.offsetLeft - parentElement.offsetLeft);");
		builder.append("totalOffsetTop = totalOffsetTop + (currentElement.offsetTop - parentElement.offsetTop);");
		builder.append("currentElement = parentElement;");
		builder.append("parentElement = currentElement.parentNode;");
		builder.append("}");
		builder.append("window.scrollTo(totalOffsetLeft - " + totalScrolls[0] + ",totalOffsetTop - " + totalScrolls[1] + ");");
		session.getEval(builder.toString());
		return firstScrollableParentLocator;
	}

	/**
	 * @param ancestorLocator
	 *          the ancestor until which the absolutePositioning has to be calculated. Set to null if the absolute positioning has to be calculated from window
	 */
	private Point getAbsolutePositioningToAncestor(String locator, String ancestorLocator) {
		if (locator == ancestorLocator) {
			return new Point(0, 0);
		}

		StringBuilder builder = new StringBuilder();

		builder.append("var theElement = this.browserbot.findElement(\"" + locator + "\");");
		builder.append("var parentElement = this.browserbot.findElement(\"" + locator + "\").parentNode;");

		if (ancestorLocator == null) {
			builder.append("var ancestorElement = null;");
		} else {
			builder.append("var ancestorElement = this.browserbot.findElement(\"" + ancestorLocator + "\");");
		}

		builder.append("var xPosition = 0;");
		builder.append("var yPosition = 0;");

		// If parent is set to null, we will perform the calculation until the last calculable parent
		builder.append("while((ancestorElement == null && parentElement.localName != 'html') || (ancestorElement != null && theElement.id != ancestorElement.id)){");
		builder.append("xPosition += theElement.offsetLeft - (parentElement.offsetLeft + parentElement.scrollLeft);");
		builder.append("yPosition += theElement.offsetTop - (parentElement.offsetTop + parentElement.scrollTop);");
		builder.append("theElement = theElement.parentNode;");
		builder.append("parentElement = theElement.parentNode;");
		builder.append("}");

		// Simulating return
		builder.append("xPosition + ',' + yPosition;");

		String result = session.getEval(builder.toString());
		return new Point(Integer.valueOf(result.split(",")[0]), Integer.valueOf(result.split(",")[1]));
	}

	/**
	 * Tries to put the element whose locator is passed in parameter to the utmost top-left possible of its scrollable ancestor.
	 * 
	 * @param currentElementLocator
	 *          the element of interest, that we wish to have in view
	 * @param ancestorElementLocator
	 *          element's ancestor. Must be scrollable for the method call to have an effect.
	 * 
	 * @return an array of 2 int, containing the displacement from element to parent, scrolls deducted.
	 */
	private int[] scrollElementToView(String currentElementLocator, String ancestorElementLocator) {
		StringBuilder builder = new StringBuilder();
		builder.append("var parentElement = this.browserbot.findElement(\"" + ancestorElementLocator + "\");");
		builder.append("var currentElement = this.browserbot.findElement(\"" + currentElementLocator + "\");");

		// Calculating the correct offset to apply to parent:
		builder.append("var totalOffsetLeft = currentElement.offsetLeft - parentElement.offsetLeft;");
		builder.append("var totalOffsetTop = currentElement.offsetTop - parentElement.offsetTop;");

		builder.append("parentElement.scrollLeft = totalOffsetLeft;");
		builder.append("parentElement.scrollTop = totalOffsetTop;");

		// Returning the scroll actually performed (helps calculating the precise position of the element afterwards).
		builder.append("parentElement.scrollLeft + ',' + parentElement.scrollTop;");

		String actualOffset = session.getEval(builder.toString());
		int[] offsets = new int[2];
		offsets[0] = Integer.valueOf(actualOffset.split(",")[0]);
		offsets[1] = Integer.valueOf(actualOffset.split(",")[1]);

		return offsets;
	}

	private void scrollWindowToLocation(int x, int y) {
		session.getEval("window.scrollTo(" + x + ", " + y + ")");
	}

	private void scrollElementToLocation(String locator, int x, int y) {
		StringBuilder builder = new StringBuilder();

		builder.append("var element = this.browserbot.findElement(\"" + locator + "\");");
		builder.append("element.scrollLeft = " + x + ";");
		builder.append("element.scrollTop = " + y + ";");

		session.getEval(builder.toString());
	}

	/**
	 * <p>
	 * To use after having moved ancestors to a specific element.<br />
	 * Will reset all the ancestors and the window to origin so as to clean the mess made for screenshot capturing.
	 * </p>
	 */
	private void resetWindowAndAncestorsToOrigin(String locator) {

		String currentElementLocator = locator;

		String parentElementLocator = getParentElementId(currentElementLocator);

		while (parentElementLocator != null) {
			if (!isScrollableElement(parentElementLocator)) {
				StringBuilder builder = new StringBuilder();

				builder.append("var element = this.browserbot.findElement(\"" + parentElementLocator + "\");");
				builder.append("element.scrollLeft = 0;");
				builder.append("element.scrollTop = 0;");

				session.getEval(builder.toString());
			}
			currentElementLocator = parentElementLocator;
			parentElementLocator = getParentElementId(parentElementLocator);
		}

		// All the ancestors of the element have been updated. Put the window on the element as well.
		session.getEval("window.scrollTo(0,0);");
	}

	private static long firstId = System.currentTimeMillis();

	private String generateUniqueId() {
		return GENERATED_ID_PREFIX + String.valueOf(firstId++);
	}

	/**
	 * @return the id of the node's parent if node has a parent, null if not. An id is generated if none was assigned to the parent element.
	 */
	private String getParentElementId(String locator) {
		StringBuilder builder = new StringBuilder();

		builder.append("var currentElement = this.browserbot.findElement(\"" + locator + "\");");
		builder.append("var parent = currentElement.parentNode;");
		builder.append("if(parent != null)");
		builder.append("{");
		builder.append("if(parent.nodeName != \"BODY\")");
		builder.append("{");
		builder.append("if(parent.id == null || parent.id == \"\")");
		builder.append("{");
		builder.append("parent.id = \"" + generateUniqueId() + "\";");
		builder.append("}");
		builder.append("}");
		builder.append("}");
		builder.append("parent.id;");
		String result = session.getEval(builder.toString());

		return "".equals(result) ? null : result;
	}

}